/*************************************************************************************************
 * Class that manages the ground and the objects that are placed on it.
 * The ground manager holds a ground object that holds GroundObjetcs. Possible indices range is [1,height]
 * and [1,width].
 * The ground manages holds given geometric elements in a queue, starting from the first element to be created
 * to the last one. The queue sets the order of elements to be picked up by the robots.
 * Elements that could not be picked by the robots, are stroed in a separate list.
 *************************************************************************************************/

package ground;

import java.util.LinkedList;
import java.util.Queue;
import systemInputOutput.SystemIO;
import geometricElement.GeoElementsFactory;
import geometricElement.GeometricElement;
import robot.*;
import geometricElement.TypeNotExistException;

public class GroundManager {

	// members
	
	// numeric values for user's options
	private static final int CREATE_ROBOTS = 1;
	private static final int CREATE_GEOMETRIC_ELEMENTS = 2;
	private static final int DO_NOT_CREATE = 3;
	
	private SystemIO userIO; // input-output-system object, uses to communicate with the user
	private GeoElementsFactory factory; // used to create the geometric elements
	private Queue<GeometricElement> geoElementsQueue; // hold the geometric elements from the first that was added to the last
	private LinkedList<Robot> robotList; // hold the robots
	private LinkedList<GeometricElement> nonPickedElements; // hold geometric elements that were not picked by the robots
	private GroundObject[][] ground; // matrix to hold the objects
	private int height; // ground's vertical dimension
	private int width; // ground's horizontal dimension
	
	
	// methods
	
	/**
	 * Constructor. Allocates members and sets ground' dimensions according to given size.
	 * The size is achieved from user using the input-output-system object 
	 */
	public GroundManager() {
		// allocate members
		userIO = new SystemIO();
		factory = new GeoElementsFactory();
		geoElementsQueue = new LinkedList<GeometricElement>();
		robotList = new LinkedList<Robot>();
		nonPickedElements = new LinkedList<GeometricElement>();
		
		// get ground's size
		int[] size = new int[2];
		size = userIO.getGroundSize();
		// allocate ground matrix
		ground = new GroundObject[size[0]+1][size[1]+1];		
		// set dimensions according to given size
		this.height = size[0];
		this.width = size[1];
	}

	/**
	 * Gets a coordinate and checks if it does not exceeds ground's dimensions
	 * @param xCord- x-axis coordinate
	 * @param yCord- y-axis coordinate
	 * @return True if given coordinate is within ground's boundaries, False if not 
	 */
	private boolean isValidPosition(int xCord, int yCord) {
		// check if exceeds ground
		if ( (xCord > width) || (xCord < 1) || (yCord > height) || (yCord < 1) ) {
			// given coordinate exceeds ground
			return false;
		}
		// given coordinate is within ground's boundaries
		return true;
	}
	
	
	/**
	 * Gets a position and basket capacity and tries to place a new robot with this basket at given position on the ground.
	 * Cannot create the robot if given position is out of ground's boundaries or there is already another robot in given position. 
	 * @param xCord- x-axis coordinate for the new robot
	 * @param yCord- y-axis coordinate for the new robot
	 * @param basketCapacity- basket capacity for the new robot
	 * @throws PositionExceedsGroundException- thrown if given position is out of ground's boundaries
	 * @throws PositionTakenException- thrown if there is already another robot in given position
	 */
	private void addRobot(int xCord, int yCord, double basketCapacity) throws PositionExceedsGroundException, PositionTakenException {
		
		// check if given position is within ground's boundaries
		if (!isValidPosition(xCord, yCord)) {
			throw new PositionExceedsGroundException(xCord, yCord);
		}
		
		// check if given position is free
		if (ground[xCord][yCord] != null) { 
			throw new PositionTakenException(xCord, yCord, "robot");
		}
		
		// create new robot with given details
		Robot newRobot = new Robot(xCord, yCord, basketCapacity);
		
		ground[xCord][yCord] = newRobot; // place on the ground
		robotList.add(newRobot); // add to robot list
	}
	
	/**
	 * Asks the user for new robot's details and tries to add such robot.
	 * If given position is out of ground's boundaries or there is already another robot in there,
	 * the robot is not created and user is notified with an error message  
	 */
	private void createRobots() {
		// get new robot's details
		double property = userIO.getRobotBasket(); // get robot's basket capacity
		int[] position = userIO.getObjectPosition(); // get robot's position				
		
		// try creating a robot with given details
		try {
			addRobot(position[0], position[1], property);					
		} catch (PositionExceedsGroundException exception) {
			// given position is out of ground's boundaries
			userIO.notifyErrorMsg(exception.getMessage());
		} catch (PositionTakenException exception) {
			// there is another robot in given position
			userIO.notifyErrorMsg(exception.getMessage());
		}		
	}
	
	/**
	 * Gets a position and property and tries to place a new geometric element with this property at given position on the ground.
	 * Cannot create the geometric element if:
	 * a. given position is out of ground's boundaries
	 * b. there is already another geometric in given position
	 * c. there is a robot in given position, that cannot pick the new element
	 * @param xCord- x-axis coordinate for the new geometric element
	 * @param yCord- y-axis coordinate for the new geometric element 
	 * @param type- new geometric element's type
	 * @param property- new geometric element's property (radius for circle, side length for square and perfect triangle
	 * @throws PositionExceedsGroundException- thrown if given position is out of ground's boundaries
	 * @throws TypeNotExistException- thrown if given type does not match a known geometric element
	 * @throws PositionTakenException- thrown if there is already another geometric in given position
	 * @throws ElementTooBigException- thrown if there is a robot in given position, that cannot pick the new element
	 */
	private void addGeometricElement(int xCord, int yCord, String type, double property) throws PositionExceedsGroundException, TypeNotExistException, PositionTakenException, ElementTooBigException {
		
		// check if given position is within ground's boundaries
		if (!isValidPosition(xCord, yCord)) {
			throw new PositionExceedsGroundException(xCord, yCord);
		}
		
		// check if given type is a valid geometric element
		GeometricElement newElement;
		try {
			newElement = factory.createElement(type, xCord, yCord, property);			
		} catch (NullPointerException exception) {
			// given type is invalid
			throw new TypeNotExistException(type);
		}
		
		// check if given position is free
		if (ground[xCord][yCord] != null) {
			
			// check if there is another geometric element at given position
			if (ground[xCord][yCord] instanceof GeometricElement) {
				throw new PositionTakenException(xCord, yCord, "geometric element");
			}
			
			// check if the robot in this position can pick up the element
			((Robot)(ground[xCord][yCord])).pickElement(property, xCord, yCord);
		}
		
		// place the element on the ground
		ground[xCord][yCord] = newElement;
		// add the element to element's queue
		geoElementsQueue.add(newElement);
	}
	
	/**
	 * Asks the user for new geometric element's details and tries to add such element.
	 * If one of the following applies, the element is not created and user is notified with an error message:
	 * a. given position is out of ground's boundaries
	 * b. there is already another geometric element in given position
	 * c. there is a robot in given position, and the robot cannot pick the new element
	 */
	private void createGeoElements() {
		// get new geometric element's details
		String type = userIO.getElementType(); // get element's type		
		double property = userIO.getElementProperty(); // get element's property
		int[] position = userIO.getObjectPosition(); // get element's position				
		
		// try creating a new geometric element with given details
		try {
			addGeometricElement(position[0], position[1], type, property);					
		} catch (PositionExceedsGroundException exception) {
			// given position is out of ground's boundaries, do not create robot and notify user
			userIO.notifyErrorMsg(exception.getMessage());
		} catch (PositionTakenException exception) {
			// there is another element in given position
			userIO.notifyErrorMsg(exception.getMessage());
		} catch (ElementTooBigException exception) {
			// there is a robot in given position, that cannot pick the new element
			userIO.notifyErrorMsg(exception.getMessage());
		} catch (TypeNotExistException exception) {
			// given type does not match a known geometric element
			userIO.notifyErrorMsg(exception.getMessage());
		}		
	}
	
	/**
	 * Gets a geometric element and scans the robot list in order to find the nearest robot that its basket can hold 
	 * the given geometric element 
	 * @param elementToPick- Geometric element to be picked
	 * @return the robot that can pick it, or null if no such robot was found
	 */
	private Robot findPickingRobot(GeometricElement elementToPick) {
		// get given geometric element's position
		int xDest = elementToPick.getXcord();
		int yDest = elementToPick.getYcord();
		Robot nearestRobot = null;
		
		/* maximal distance is length of ground's diagonal- therefore there is no such robot with maximal distance + 1.
		 * this value will allow to know whether a robot with a basket big enough, for the geometric element, was found
		 */
		double minDistance = Math.sqrt(height*height+width*width) + 1;
		
		// scan robot list
		for (Robot currRobot : robotList) {
			// check if current robot's basket is big enough
			if (currRobot.canPickElement(elementToPick)) {
				// current robot can pick it, check if current robot is the nearest found 
				if (currRobot.calculateRealDisTanceFromDot(xDest, yDest) < minDistance) {
					// current robot is the nearest
					minDistance = currRobot.calculateRealDisTanceFromDot(xDest, yDest); // update minimal distance
					nearestRobot = currRobot; // update nearest robot pointer
				}
			}
		}
		// done scanning, return nearest robot
		return nearestRobot;
	}
	
	/**
	 * Asks the user whether to place new robots on the ground. Then asks the user whether to place new geometric elements on the ground.
	 * The user can choose not to place robots nor elements at all.
	 */
	public void placeObjects() {
		
		// get option from user
		int option = userIO.getOption();
		
		boolean keepRunning = true; // flag to keep looping		
		while (keepRunning) {
			
			// perform task according to user's choice
			switch (option) {
			
			case CREATE_ROBOTS:
				boolean keepCreating = true; // flag to keep running in the loop
				while (keepCreating) {
					// add a new robot
					createRobots();
					// if user wants to add more robots, keep looping 
					option = userIO.askIfContinueAdding("robot");				
					if (option == 0) {
						keepCreating = false;
					}				
				}
				
				// if user wants to add geometric elements, move to next case
				if( userIO.askToAddGeoElements() == 0) {		
					break;
				}
			
			case CREATE_GEOMETRIC_ELEMENTS:
				
				keepCreating = true; // initialize flag		
				while (keepCreating) {
					// add a new geometric element
					createGeoElements();
					// if user wants to add more elements, keep looping
					option = userIO.askIfContinueAdding("geometric element");				
					if (option == 0) {
						keepCreating = false;
					}				
				}
				// done tasks, end loop
				keepRunning = false;
				break;
				
			case DO_NOT_CREATE:
				// user does not want to add any object, end loop
				keepRunning = false;
				break;
	
			default:
				// invalid option value was given, notify user and try again
				userIO.notifyErrorMsg("No such option, please try again");
				// ask user again for option
				option = userIO.getOption();
				break;
			}		
		}
		
	}
	
	/**
	 * For each geometric element, for the first that was added to the last, searches the nearest robot that can pick it.
	 * If there is such robot, it picks it and the element is removed from the ground. If not, the element is remains on ground
	 * and inserted to the list of elements that could not be picked  
	 */
	public void pickUpElements() {
		// scan queue until no geometric element is left
		while (geoElementsQueue.peek() != null) {
			// get first element on queue
			GeometricElement currElement = geoElementsQueue.poll();
			// find the robot that should pick the element
			Robot pickingRobot = findPickingRobot(currElement);
			
			if (pickingRobot != null) { // check if such robot was found
				try {
					// backup current robot's position
					int oldRobotXcord = pickingRobot.getXcord();
					int oldRobotYcord = pickingRobot.getYcord();
					// try picking the element
					pickingRobot.pickElement(currElement.getArea(), currElement.getXcord(), currElement.getYcord());
					// picked element- now update the ground
					ground[oldRobotXcord][oldRobotYcord] = null; // the robot has left its previous position
					ground[currElement.getXcord()][currElement.getYcord()] = pickingRobot; // replace the element with the robot
				} catch (Exception e) {
					// TODO: handle exception
				}
			} else {
				// no robot can pick this element, add it to list 
				nonPickedElements.add(currElement);
			}
		}
	}
	
	/**
	 * Prints the list of all elements that were not picked by the robots
	 */
	public void printRemainingElements() {
		userIO.printNonPickedGeoElements(nonPickedElements);		
	}
	
	/**
	 * Prints the statistics of all robots. For each robot, the printed details are:
	 * current position on ground, real distance the robot passed, number of paces made and capacity left in robot's basket
	 */
	public void printRobotStatistics() {
		userIO.printRobotsStats(robotList);
	}

}
